package com.lehecai.ucenter.utils;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import com.lehecai.core.util.CharsetConstant;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.Random;

/**
 * Java Server side class for Google Authenticator's TOTP generator 
 * Thanks to Enrico's blog for the sample code:
 * @see <a href="http://thegreyblog.blogspot.com/2011/12/google-authenticator-using-it-in-your.html">http://thegreyblog.blogspot.com/2011/12/google-authenticator-using-it-in-your.html</a>
 * @see <a href="http://code.google.com/p/google-authenticator">http://code.google.com/p/google-authenticator</a>
 * @see <a href="http://tools.ietf.org/id/draft-mraihi-totp-timebased-06.txt">http://tools.ietf.org/id/draft-mraihi-totp-timebased-06.txt</a>
 */
public class GoogleAuthenticator {
    private static final Logger logger = LoggerFactory.getLogger(GoogleAuthenticator.class);
		
	// taken from Google pam docs - we probably don't need to mess with these
	final static int secretSize = 10;
//	final static int numOfScratchCodes = 5;
//	final static int scratchCodeSize = 8;
	
	int window_size =3;  // default 3 - max 17 (from google docs)
	
	/**
	 * set the windows size. This is an integer value representing the number of 30 second windows we allow
	 * The bigger the window, the more tolerant of clock skew we are. 
	 * 
	 * @param s window size - must be >=1 and <=17.  Other values are ignored
	 */
	public void setWindowSize(int s) {
		if( s >= 1 && s <= 17 )
			window_size = s;
	}

	/**
	 * Generate a random secret key. This must be saved by the server and associated with the 
	 * users account to verify the code displayed by Google Authenticator. 
	 * The user must register this secret on their device. 
	 * @return secret key
	 */
	public static String generateSecretKey(String password) {
		// Allocating the buffer
//		byte[] buffer = new byte[secretSize + numOfScratchCodes* scratchCodeSize];

		// Filling the buffer with random numbers.
		// Notice: you want to reuse the same random generator
		// while generating larger random number sequences.
//		new Random().nextBytes(buffer);

        if (StringUtils.isEmpty(password)) {
            logger.error("生成动态密码密钥时，密码参数不能空");
            return null;
        }

        Random r = new Random();
        String key = password + r.nextLong();
        byte[] b = DigestUtils.md5Hex(key).getBytes();

		// Getting the key and converting it to Base32
		Base32 base32 = new Base32();
		byte[] secretKey = Arrays.copyOf(b, secretSize);
		byte[] bEncodedKey = base32.encode(secretKey);
		String encodedKey = new String(bEncodedKey);

		return encodedKey;
	}

	/**
	 * Return a URL that generates and displays a QR barcode. The user scans this bar code with the
	 * Google Authenticator application on their smartphone to register the auth code. They can also manually enter the
	 * secret if desired
	 * 
	 * @param user   user id (e.g. fflinstone)
	 * @param host   host or system that the code is for (e.g. myapp.com)
	 * @param secret the secret that was previously generated for this user
	 * @return the URL for the QR code to scan
	 */
	public static String getRemoteQRBarcodeURL(String user, String host, String secret) {
		String format = "https://www.google.com/chart?chs=200x200&chld=M%%7C0&cht=qr&chl=otpauth://totp/%s@%s%%3Fsecret%%3D%s";
//		String format = "https://chart.googleapis.com/chart?chs=200x200&chld=M%%7C0&cht=qr&chl=otpauth://totp/%s@%s%%3Fsecret%%3D%s";
		return String.format(format, user, host, secret);
	}

    public static ByteArrayOutputStream generateLocalQRBarcode(String user, String host, String secret) {
        int width = 200;
        int height = 200;
        String content = String.format("otpauth://totp/%s@%s?secret=%s", user, host, secret);

        Hashtable<EncodeHintType, Object> hints = new Hashtable<EncodeHintType, Object>();
        //设置字符编码
        hints.put(EncodeHintType.CHARACTER_SET, CharsetConstant.CHARSET_UTF8);
        // 指定纠错等级
        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
        BitMatrix matrix = null;
        try {
            matrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints);
        } catch (WriterException e) {
            logger.error(e.getMessage(), e);
        }
        if (matrix == null) {
            logger.error("生成二维码出错。");
            return null;
        }

//        MatrixToImageConfig logoConfig = new MatrixToImageConfig();
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(4096);
        try {
            MatrixToImageWriter.writeToStream(matrix, "jpg", byteArrayOutputStream);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        return byteArrayOutputStream;
    }

	/**
	 * Check the code entered by the user to see if it is valid
	 * @param secret  The users secret. 
	 * @param code  The code displayed on the users device
	 * @param timeMsec  The time in msec (System.currentTimeMillis() for example)
	 * @return
	 */
	public boolean check_code(String secret, long code, long timeMsec) {
		Base32 codec = new Base32();
		byte[] decodedKey = codec.decode(secret);
			
		// convert unix msec time into a 30 second "window" 
		// this is per the TOTP spec (see the RFC for details)
		long t = (timeMsec / 1000L) / 30L;
		// Window is used to check codes generated in the near past.
		// You can use this value to tune how far you're willing to go.
		
		for (int i = -window_size; i <= window_size; ++i) {
			long hash;
			try {
				hash = verify_code(decodedKey, t + i);
			} catch (Exception e) {
				// Yes, this is bad form - but
				// the exceptions thrown would be rare and a static configuration problem
				e.printStackTrace();
				throw new RuntimeException(e.getMessage());
				//return false;
			}
			if (hash == code) {
				return true;
			}
		}
		// The validation code is invalid.
		return false;
	}

	
	private static int verify_code(byte[] key, long t)
			throws NoSuchAlgorithmException, InvalidKeyException {
		byte[] data = new byte[8];
		long value = t ;
		for (int i = 8; i-- > 0; value >>>= 8) {
			data[i] = (byte) value;
		}

		SecretKeySpec signKey = new SecretKeySpec(key, "HmacSHA1");
		Mac mac = Mac.getInstance("HmacSHA1");
		mac.init(signKey);
		byte[] hash = mac.doFinal(data);

		int offset = hash[20 - 1] & 0xF;

		// We're using a long because Java hasn't got unsigned int.
		long truncatedHash = 0;
		for (int i = 0; i < 4; ++i) {
			truncatedHash <<= 8;
			// We are dealing with signed bytes:
			// we just keep the first byte.
			truncatedHash |= (hash[offset + i] & 0xFF);
		}

		truncatedHash &= 0x7FFFFFFF;
		truncatedHash %= 1000000;

		return (int) truncatedHash;
	}
}
